iOS里synchronized的死锁、性能、实效

synchronized的死锁

我们已经习惯了使用synchronized来做同步锁,但在实际开发中发现,写得不好容易出现死锁的坑。比如像下面这样的代码有两个个锁被公用,在多线程的情况下就容易导致死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#import "BTSynchronizedObjectA.h"
@implementation BTSynchronizedObjectA
- (void)dosomethingInA {
@synchronized(self){
NSLog(@"dosomethingInA");
}
}
@end
#import "BTSynchronizedObjectB.h"
@implementation BTSynchronizedObjectB
- (void)callA {
NSLog(@"call A begin");
[self.shareLock lock];
[self.synchronizedObjectA dosomethingInA];
[self.shareLock unlock];
NSLog(@"call A end");
}
- (void)dosomethingInB {
NSLog(@"dosomethingInB begin");
@synchronized(self.synchronizedObjectA) {
[self.shareLock lock];
NSLog(@"dosomethingInB");
[self.shareLock unlock];
}
NSLog(@"dosomethingInB begin");
}
@end

怎么写可以避免这种情况出现呢?改写的方法是在用到synchronized时传入一个内部NSObject成员变量,外部不能获取到这个变量,就不会形成交替死锁。

1
2
3
4
5
6
7
8
9
#import "BTSynchronizedObjectA.h"
@implementation BTSynchronizedObjectA
- (void)dosomethingInA {
@synchronized(self.tokenA){
NSLog(@"dosomethingInA");
}
}
@end

synchronized的性能

synchronized锁本身性能是会比其他锁要慢,有人对此做过测试。但是在实际使用中,锁本身的性能差异几乎可以忽略不计。
那为什么有人会抱怨用synchronized性能不好呢?

1
2
3
4
5
6
- (void)testPerformance {
@synchronized(self.token) {
// do some thing
[self dosomethingelse];
}
}

像上面这段被加锁的代码里,通过[self dosomethingelse]再调用别的方法。dosomethingelse方法的开发者,很可能并不知道自己的方法被锁同步的,他又可能调用别的函数,这样一层一层调用就可能变得更慢。
所以使用synchronized时一定要注意,尽量减少锁的范围和粒度。

不同数据使用不同锁,控制最小的粒度

1
2
3
4
5
6
7
@synchronized (tokenA) {
// do some thing
}
@synchronized (tokenB) {
// do some thing
}

减小加锁的范围,不必要加锁的代码放到外面

1
2
3
4
5
6
@synchronized (tokenA) {
// do some thing need locked
}
// do some thing else
[self dosomethingelse];
...

synchronized的失效

用synchronized时需要传入一个object,你有没有想过如果你传入的是nil会怎么样呢?
根据汇编代码可以发现,synchronized实现里会调用objc_sync_enter和objc_sync_exit

1
2
3
4
5
{
@synchronized(self) {
return [[myString retain] autorelease];
}
}

转换为

1
2
3
4
5
6
{
objc_sync_enter(self)
id retVal = [[myString retain] autorelease];
objc_sync_exit(self);
return retVal;
}

objc_sync_enter和objc_sync_exit函数定义在
其中objc_sync_enter函数的实现里,正常的情况obj不为nil时,会根据obj内存地址的哈希值查找合适的SyncData然后使用递归mutex加锁recursive_mutex_lock。到了objc_sync_exit时同样通过obj的内存地址的哈希值查找合适的SyncData,然后将其解锁recursive_mutex_unlock。

But,当执行objc_sync_enter函数时,如果传入的obj为nil,那并不会加锁,直接走到objc_sync_nil。也就是说,如果使用@synchronized时传入的obj为nil,那将不会加锁也就是失去了同步的效果。所以一定要保证obj不在执行期间被设置被nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
result = recursive_mutex_lock(&data->mutex);
require_noerr_string(result, done, "mutex_lock failed");
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
done:
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR, "id2data failed");
result = recursive_mutex_unlock(&data->mutex);
require_noerr_string(result, done, "mutex_unlock failed");
} else {
// @synchronized(nil) does nothing
}
done:
if ( result == RECURSIVE_MUTEX_NOT_LOCKED )
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
return result;
}

在工程里,将obj设置为nil,给objc_sync_nil加个断点,会看到确实走到objc_sync_nil函数

参考

synchronized的实现
objc-sync源码
关于 @synchronized,这儿比你想知道的还要多
正确使用多线程同步锁@synchronized()

Blacktea wechat
ex. subscribe to my blog by scanning my public wechat account
记录生活于感悟,您的支持将鼓励我继续创作!